'use strict';

(() => {
  /* Chrome reinjects content script when documentElement is replaced so we ignore it
   by checking against a literal `1`, not just `if (truthy)`, because <html id="INJECTED">
   is exposed per HTML spec as a global `window.INJECTED` */
  if (window.INJECTED === 1) return;

  //#region for content scripts and our extension pages

  if (!((window.browser || {}).runtime || {}).sendMessage) {
    /* Auto-promisifier with a fallback to direct call on signature error.
       The fallback isn't used now since we call all synchronous methods via `chrome` */
    const directEvents = ['addListener', 'removeListener', 'hasListener', 'hasListeners'];
    // generated by tools/chrome-api-no-cb.js
    const directMethods = {
      alarms: ['create'],
      extension: ['getBackgroundPage', 'getExtensionTabs', 'getURL', 'getViews', 'setUpdateUrlData'],
      i18n: ['getMessage', 'getUILanguage'],
      identity: ['getRedirectURL'],
      runtime: ['connect', 'connectNative', 'getManifest', 'getURL', 'reload', 'restart'],
      tabs: ['connect'],
    };
    const promisify = function (fn, ...args) {
      let res;
      let resolve, reject;
      // Saving the local callstack before making an async call
      const err = new Error();
      try {
        args.push((...results) => {
          const {lastError} = chrome.runtime;
          if (lastError) {
            err.message = lastError.message;
            reject(err);
          } else {
            /* Some callbacks have 2 parameters so we're resolving as an array in that case.
               For example, chrome.runtime.requestUpdateCheck and chrome.webRequest.onAuthRequired */
            resolve(results.length <= 1 ? results[0] : results);
          }
        });
        fn.apply(this, args);
        res = new Promise((...rr) => ([resolve, reject] = rr));
      } catch (err) {
        if (!err.message.includes('No matching signature')) {
          throw err;
        }
        args.pop();
        res = fn.apply(this, args);
      }
      return res;
    };
    const proxify = (src, srcName, target, key) => {
      let res = src[key];
      if (res && typeof res === 'object') {
        res = createProxy(res, key); // eslint-disable-line no-use-before-define
      } else if (typeof res === 'function') {
        res = (directMethods[srcName] || directEvents).includes(key)
          ? res.bind(src)
          : promisify.bind(src, res);
      }
      target[key] = res;
      return res;
    };
    const createProxy = (src, srcName) =>
      new Proxy({}, {
        get(target, key) {
          return target[key] || proxify(src, srcName, target, key);
        },
      });
    window.browser = createProxy(chrome);
  }

  //#endregion

  if (!chrome.tabs) return;

  //#region for our extension pages

  const reqPromise = {};
  window.require = async function require(urls, cb) {
    const promises = [];
    const all = [];
    const toLoad = [];
    for (let url of Array.isArray(urls) ? urls : [urls]) {
      const isCss = url.endsWith('.css');
      const tag = isCss ? 'link' : 'script';
      const attr = isCss ? 'href' : 'src';
      if (!isCss && !url.endsWith('.js')) url += '.js';
      if (url[0] === '/' && location.pathname.indexOf('/', 1) < 0) url = url.slice(1);
      let el = document.head.querySelector(`${tag}[${attr}$="${url}"]`);
      if (!el) {
        el = document.createElement(tag);
        toLoad.push(el);
        reqPromise[url] = new Promise((resolve, reject) => {
          el.onload = resolve;
          el.onerror = reject;
          el[attr] = url;
          if (isCss) el.rel = 'stylesheet';
        }).catch(console.warn);
      }
      promises.push(reqPromise[url]);
      all.push(el);
    }
    if (toLoad.length) document.head.append(...toLoad);
    if (promises.length) await Promise.all(promises);
    if (cb) cb(...all);
    return all[0];
  };

  if (!(new URLSearchParams({foo: 1})).get('foo')) {
    // TODO: remove when minimum_chrome_version >= 61
    window.URLSearchParams = class extends URLSearchParams {
      constructor(init) {
        if (init && typeof init === 'object') {
          super();
          for (const [key, val] of Object.entries(init)) {
            this.set(key, val);
          }
        } else {
          super(...arguments);
        }
      }
    };
  }

  //#endregion
})();
